package edu.hda.pt.utils.connections.netty.codec;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.frame.FrameDecoder;

import edu.hda.pt.client.Config;
import edu.hda.pt.utils.Stream;
import edu.hda.pt.utils.TestObject;
import edu.hda.pt.utils.Stream.StreamState;
import edu.hda.pt.utils.connections.netty.NettyConstants;

/**
 * This class decodes recieved messages
 * 
 * @author Marc Hammerton
 * 
 */
public class MessageDecoder extends FrameDecoder {

	private int type = 0;
	private int header_length = 0;
	private int bodyLength = 0;

	/**
	 * This method decodes a received message. The method can be called multiple
	 * times until the whole message is received. It determines the type of the
	 * message and then extracts the value.
	 * 
	 * @see org.jboss.netty.handler.codec.frame.FrameDecoder#decode(org.jboss.netty.channel.ChannelHandlerContext,
	 *      org.jboss.netty.channel.Channel,
	 *      org.jboss.netty.buffer.ChannelBuffer)
	 */
	@Override
	protected Object decode(ChannelHandlerContext chx, Channel channel,
			ChannelBuffer buffer) throws Exception {
		// Wait until at least the short header is available
		if (buffer.readableBytes() < NettyConstants.HEADER_LENGTH_SHORT) {
			return null;
		}

		this.type = buffer.getShort(0);
		if (type == Config.HANDLEBYTES || type == Config.HANDLEOBJECT
				|| type == Config.HANDLESTRING) {

			// Wait until the long header is available
			if (buffer.readableBytes() < NettyConstants.HEADER_LENGTH_SHORT) {
				return null;
			}

			this.bodyLength = buffer.getInt(2);
			this.header_length = NettyConstants.HEADER_LENGTH_LONG;
		} else if (type == Config.HANDLEBOOL) {
			this.bodyLength = 1;
			this.header_length = NettyConstants.HEADER_LENGTH_SHORT;
		} else if (type == Config.HANDLEINTEGER) {
			this.bodyLength = 4;
			this.header_length = NettyConstants.HEADER_LENGTH_SHORT;
		} else if (type == Config.HANDLELONG) {
			this.bodyLength = 8;
			this.header_length = NettyConstants.HEADER_LENGTH_SHORT;
		} else if (type == Config.HANDLESTREAM_START) {
			this.bodyLength = 4;
			this.header_length = NettyConstants.HEADER_LENGTH_SHORT;
		} else if (type == Config.HANDLESTREAM_STARTED) {
			this.bodyLength = 0;
			this.header_length = NettyConstants.HEADER_LENGTH_SHORT;
		} else if (type == Config.HANDLESTREAM_STOP) {
			this.bodyLength = 0;
			this.header_length = NettyConstants.HEADER_LENGTH_SHORT;
		}

		// Wait until the whole data is available.
		if (buffer.readableBytes() < bodyLength + this.header_length) {
			return null;
		}

		// Skip the length field because we know it already.
		buffer.skipBytes(this.header_length);

		Object value;
		switch (type) {
			case Config.HANDLEBOOL:
				value = decodeBool(buffer);
				break;
			case Config.HANDLEBYTES:
				value = decodeBytes(buffer);
				break;
			case Config.HANDLEINTEGER:
				value = decodeInt(buffer);
				break;
			case Config.HANDLELONG:
				value = decodeLong(buffer);
				break;
			case Config.HANDLEOBJECT:
				value = decodeObject(buffer);
				break;
			case Config.HANDLESTRING:
				value = decodeString(buffer);
				break;
			case Config.HANDLESTREAM_START:
				value = decodeStreamStart(buffer);
				break;
			case Config.HANDLESTREAM_STARTED:
				value = decodeStreamStarted();
				break;
			case Config.HANDLESTREAM_STOP:
				value = decodeStreamStop();
				break;
			default:
				value = null;
				break;
		}

		return value;
	}

	/**
	 * Extracts a Boolean value from the buffer
	 * 
	 * @param buffer
	 *            - Containing the value
	 * @return the Boolean value
	 */
	private boolean decodeBool(ChannelBuffer buffer) {
		boolean value = buffer.readByte() == 1 ? true : false;
		return value;
	}

	/**
	 * Extracts a Byte value from the buffer
	 * 
	 * @param buffer
	 *            - Containing the value
	 * @return the Byte value
	 */
	private byte[] decodeBytes(ChannelBuffer buffer) {
		byte[] value = new byte[this.bodyLength];
		buffer.readBytes(value);

		return value;
	}

	/**
	 * Extracts an Integer value from the buffer
	 * 
	 * @param buffer
	 *            - Containing the value
	 * @return the Integer value
	 */
	private int decodeInt(ChannelBuffer buffer) {
		return buffer.readInt();
	}

	/**
	 * Extracts a Long value from the buffer
	 * 
	 * @param buffer
	 *            - Containing the value
	 * @return the Long value
	 */
	private long decodeLong(ChannelBuffer buffer) {
		return buffer.readLong();
	}

	/**
	 * Extracts an object from the buffer
	 * 
	 * @param buffer
	 *            - Containing the value
	 * @return the object
	 */
	private TestObject decodeObject(ChannelBuffer buffer) {
		byte[] bytes = new byte[this.bodyLength];
		buffer.readBytes(bytes);

		// Convert ByteArray to Object
		TestObject value = null;
		try {
			ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
			ObjectInputStream ois = new ObjectInputStream(bis);
			value = (TestObject) ois.readObject();
			ois.close();
			bis.close();
		} catch (IOException e) {
			System.out.println("IOException in Netty Message Decoder: ");
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			System.out
					.println("ClassNotFoundException in Netty Message Decoder: ");
			e.printStackTrace();
		}

		return value;
	}

	/**
	 * Extracts a String value from the buffer
	 * 
	 * @param buffer
	 *            - Containing the value
	 * @return the String value
	 */
	private String decodeString(ChannelBuffer buffer) {
		byte[] bytes = new byte[bodyLength];
		buffer.readBytes(bytes);
		String value = new String(bytes);

		return value;
	}

	/**
	 * Extracts the frame size and sets the stream state to START
	 * 
	 * @param buffer
	 *            - Containing the frame size
	 * @return the stream object
	 */
	private Stream decodeStreamStart(ChannelBuffer buffer) {
		int fs = buffer.readInt();

		Stream stream = new Stream(fs);
		stream.setState(StreamState.START);
		return stream;
	}

	/**
	 * Creates a new Stream object and sets the state to RUNNING
	 * 
	 * @return the stream object
	 */
	private Stream decodeStreamStarted() {
		Stream stream = new Stream();
		stream.setState(StreamState.RUNNING);
		return stream;
	}

	/**
	 * Creates a new stream object and sets the sate to FINISH
	 * 
	 * @return the stream object
	 */
	private Stream decodeStreamStop() {
		Stream stream = new Stream();
		stream.setState(StreamState.FINISH);
		return stream;
	}

}
